Explorez la communication cross-origine sécurisée avec l'API PostMessage. Découvrez ses capacités, ses risques de sécurité et les meilleures pratiques pour atténuer les vulnérabilités.
Communication Cross-Origine : Modèles de Sécurité avec l'API PostMessage
Dans le web moderne, les applications ont fréquemment besoin d'interagir avec des ressources provenant de différentes origines. La Politique de Même Origine (Same-Origin Policy - SOP) est un mécanisme de sécurité crucial qui empêche les scripts d'accéder à des ressources d'une origine différente. Cependant, il existe des scénarios légitimes où la communication cross-origine est nécessaire. L'API postMessage fournit un mécanisme contrôlé pour y parvenir, mais il est vital de comprendre ses risques de sécurité potentiels et de mettre en œuvre des modèles de sécurité appropriés.
Comprendre la Politique de MĂŞme Origine (SOP)
La Politique de Même Origine est un concept de sécurité fondamental dans les navigateurs web. Elle empêche les pages web d'effectuer des requêtes vers un domaine différent de celui qui a servi la page web. Une origine est définie par le schéma (protocole), l'hôte (domaine) et le port. Si l'un de ces éléments diffère, les origines sont considérées comme différentes. Par exemple :
https://example.comhttps://www.example.comhttp://example.comhttps://example.com:8080
Ce sont toutes des origines différentes, et la SOP restreint l'accès direct par script entre elles.
Présentation de l'API PostMessage
L'API postMessage fournit un mécanisme sûr et contrôlé pour la communication cross-origine. Elle permet aux scripts d'envoyer des messages à d'autres fenêtres (par exemple, des iframes, de nouvelles fenêtres ou des onglets), quelle que soit leur origine. La fenêtre de réception peut alors écouter ces messages et les traiter en conséquence.
La syntaxe de base pour envoyer un message est :
otherWindow.postMessage(message, targetOrigin);
otherWindow: Une référence à la fenêtre cible (par exemple,window.parent,iframe.contentWindow, ou un objet de fenêtre obtenu viawindow.open).message: Les données que vous souhaitez envoyer. Il peut s'agir de n'importe quel objet JavaScript pouvant être sérialisé (par exemple, des chaînes de caractères, des nombres, des objets, des tableaux).targetOrigin: Spécifie l'origine à laquelle vous souhaitez envoyer le message. C'est un paramètre de sécurité crucial.
Du côté de la réception, vous devez écouter l'événement message :
window.addEventListener('message', function(event) {
// ...
});
L'objet event contient les propriétés suivantes :
event.data: Le message envoyé par l'autre fenêtre.event.origin: L'origine de la fenêtre qui a envoyé le message.event.source: Une référence à la fenêtre qui a envoyé le message.
Risques de Sécurité et Vulnérabilités
Bien que postMessage offre un moyen de contourner les restrictions de la SOP, il introduit également des risques de sécurité potentiels s'il n'est pas mis en œuvre avec soin. Voici quelques vulnérabilités courantes :
1. Non-concordance de l'Origine Cible
Le fait de ne pas valider la propriété event.origin est une vulnérabilité critique. Si le récepteur fait aveuglément confiance au message, n'importe quel site web peut envoyer des données malveillantes. Vérifiez toujours que l'event.origin correspond à l'origine attendue avant de traiter le message.
Exemple (Code Vulnérable) :
window.addEventListener('message', function(event) {
// NE FAITES PAS CELA !
processMessage(event.data);
});
Exemple (Code Sécurisé) :
window.addEventListener('message', function(event) {
if (event.origin !== 'https://trusted-origin.com') {
console.warn('Message reçu d\'une origine non fiable :', event.origin);
return;
}
processMessage(event.data);
});
2. Injection de Données
Traiter les données reçues (event.data) comme du code exécutable ou les injecter directement dans le DOM peut conduire à des vulnérabilités de Cross-Site Scripting (XSS). Nettoyez et validez toujours les données reçues avant de les utiliser.
Exemple (Code Vulnérable) :
window.addEventListener('message', function(event) {
if (event.origin === 'https://trusted-origin.com') {
document.body.innerHTML = event.data; // NE FAITES PAS CELA !
}
});
Exemple (Code Sécurisé) :
window.addEventListener('message', function(event) {
if (event.origin === 'https://trusted-origin.com') {
const sanitizedData = sanitize(event.data); // Implémentez une fonction de nettoyage appropriée
document.getElementById('message-container').textContent = sanitizedData;
}
});
function sanitize(data) {
// Implémentez une logique de nettoyage robuste ici.
// Par exemple, utilisez DOMPurify ou une bibliothèque similaire
return DOMPurify.sanitize(data);
}
3. Attaques de l'Homme du Milieu (MITM)
Si la communication se fait sur un canal non sécurisé (HTTP), un attaquant MITM peut intercepter et modifier les messages. Utilisez toujours HTTPS pour une communication sécurisée.
4. Cross-Site Request Forgery (CSRF)
Si le récepteur effectue des actions basées sur le message reçu sans validation appropriée, un attaquant pourrait potentiellement forger des messages pour tromper le récepteur et lui faire effectuer des actions non intentionnelles. Mettez en œuvre des mécanismes de protection CSRF, comme l'inclusion d'un jeton secret dans le message et sa vérification côté récepteur.
5. Utilisation de Caractères Joker dans targetOrigin
Définir targetOrigin à * permet à n'importe quelle origine de recevoir le message. Cela doit être évité sauf si c'est absolument nécessaire, car cela va à l'encontre de l'objectif de la sécurité basée sur l'origine. Si vous devez utiliser *, assurez-vous de mettre en œuvre d'autres mesures de sécurité solides, telles que les codes d'authentification de message (MAC).
Exemple (À éviter) :
otherWindow.postMessage(message, '*'); // Évitez d'utiliser '*' sauf si c'est absolument nécessaire
Modèles de Sécurité et Meilleures Pratiques
Pour atténuer les risques associés à postMessage, suivez ces modèles de sécurité et meilleures pratiques :
1. Validation Stricte de l'Origine
Validez toujours la propriété event.origin du côté du récepteur. Comparez-la à une liste prédéfinie d'origines de confiance. Utilisez l'égalité stricte (===) pour la comparaison.
2. Nettoyage et Validation des Données
Nettoyez et validez toutes les données reçues via postMessage avant de les utiliser. Utilisez des techniques de nettoyage appropriées en fonction de la manière dont les données seront utilisées (par exemple, échappement HTML, encodage d'URL, validation des entrées). Utilisez des bibliothèques comme DOMPurify pour nettoyer le HTML.
3. Codes d'Authentification de Message (MAC)
Incluez un Code d'Authentification de Message (MAC) dans le message pour garantir son intégrité et son authenticité. L'expéditeur calcule le MAC à l'aide d'une clé secrète partagée et l'inclut dans le message. Le récepteur recalcule le MAC en utilisant la même clé secrète partagée et le compare avec le MAC reçu. S'ils correspondent, le message est considéré comme authentique et non altéré.
Exemple (Utilisation de HMAC-SHA256) :
// Émetteur
async function sendMessage(message, targetOrigin, sharedSecret) {
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(message));
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(sharedSecret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
const signature = await crypto.subtle.sign("HMAC", key, data);
const signatureArray = Array.from(new Uint8Array(signature));
const signatureHex = signatureArray.map(b => b.toString(16).padStart(2, '0')).join('');
const securedMessage = {
data: message,
signature: signatureHex
};
otherWindow.postMessage(securedMessage, targetOrigin);
}
// Récepteur
async function receiveMessage(event, sharedSecret) {
if (event.origin !== 'https://trusted-origin.com') {
console.warn('Message reçu d\'une origine non fiable :', event.origin);
return;
}
const securedMessage = event.data;
const message = securedMessage.data;
const receivedSignature = securedMessage.signature;
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(message));
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(sharedSecret),
{ name: "HMAC", hash: "SHA-256" },
false,
["verify"]
);
const signature = await crypto.subtle.sign("HMAC", key, data);
const signatureArray = Array.from(new Uint8Array(signature));
const signatureHex = signatureArray.map(b => b.toString(16).padStart(2, '0')).join('');
if (signatureHex === receivedSignature) {
console.log('Le message est authentique !');
processMessage(message); // Procéder au traitement du message
} else {
console.error('La vérification de la signature du message a échoué !');
}
}
Important : La clé secrète partagée doit être générée et stockée de manière sécurisée. Évitez de coder en dur la clé dans le code.
4. Utilisation de Nonce et d'Horodatages
Pour prévenir les attaques par rejeu, incluez un nonce unique (nombre utilisé une seule fois) et un horodatage dans le message. Le récepteur peut alors vérifier que le nonce n'a pas été utilisé auparavant et que l'horodatage se situe dans un délai acceptable. Cela atténue le risque qu'un attaquant rejoue des messages précédemment interceptés.
5. Principe du Moindre Privilège
N'accordez que les privilèges minimaux nécessaires à l'autre fenêtre. Par exemple, si l'autre fenêtre n'a besoin que de lire des données, ne l'autorisez pas à en écrire. Concevez votre protocole de communication en gardant à l'esprit le principe du moindre privilège.
6. Content Security Policy (CSP)
Utilisez la Content Security Policy (CSP) pour restreindre les sources à partir desquelles les scripts peuvent être chargés et les actions que les scripts peuvent effectuer. Cela peut aider à atténuer l'impact des vulnérabilités XSS qui pourraient découler d'une mauvaise gestion des données postMessage.
7. Validation des Entrées
Validez toujours la structure et le format des données reçues. Définissez un format de message clair et assurez-vous que les données reçues sont conformes à ce format. Cela aide à prévenir les comportements inattendus et les vulnérabilités.
8. Sérialisation Sécurisée des Données
Utilisez un format de sérialisation de données sécurisé, tel que JSON, pour sérialiser et désérialiser les messages. Évitez d'utiliser des formats qui permettent l'exécution de code, tels que eval() ou Function().
9. Limiter la Taille des Messages
Limitez la taille des messages envoyés via postMessage. Les messages volumineux peuvent consommer des ressources excessives et potentiellement conduire à des attaques par déni de service.
10. Audits de Sécurité Réguliers
Effectuez des audits de sécurité réguliers de votre code pour identifier et corriger les vulnérabilités potentielles. Portez une attention particulière à la mise en œuvre de postMessage et assurez-vous que toutes les meilleures pratiques de sécurité sont suivies.
Scénario d'Exemple : Communication Sécurisée entre une Iframe et son Parent
Considérons un scénario où une iframe hébergée sur https://iframe.example.com doit communiquer avec sa page parente hébergée sur https://parent.example.com. L'iframe doit envoyer des données utilisateur à la page parente pour traitement.
Iframe (https://iframe.example.com) :
// Générer une clé secrète partagée (remplacer par une méthode de génération de clé sécurisée)
const sharedSecret = 'VOTRE_SECRET_PARTAGE_SECURISE';
// Obtenir les données de l'utilisateur
const userData = {
name: 'John Doe',
email: 'john.doe@example.com'
};
// Envoyer les données de l'utilisateur à la page parente
async function sendUserData(userData) {
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(userData));
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(sharedSecret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
const signature = await crypto.subtle.sign("HMAC", key, data);
const signatureArray = Array.from(new Uint8Array(signature));
const signatureHex = signatureArray.map(b => b.toString(16).padStart(2, '0')).join('');
const securedMessage = {
data: userData,
signature: signatureHex
};
parent.postMessage(securedMessage, 'https://parent.example.com');
}
sendUserData(userData);
Page Parente (https://parent.example.com) :
// Clé secrète partagée (doit correspondre à la clé de l'iframe)
const sharedSecret = 'VOTRE_SECRET_PARTAGE_SECURISE';
window.addEventListener('message', async function(event) {
if (event.origin !== 'https://iframe.example.com') {
console.warn('Message reçu d\'une origine non fiable :', event.origin);
return;
}
const securedMessage = event.data;
const userData = securedMessage.data;
const receivedSignature = securedMessage.signature;
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(userData));
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(sharedSecret),
{ name: "HMAC", hash: "SHA-256" },
false,
["verify"]
);
const signature = await crypto.subtle.sign("HMAC", key, data);
const signatureArray = Array.from(new Uint8Array(signature));
const signatureHex = signatureArray.map(b => b.toString(16).padStart(2, '0')).join('');
if (signatureHex === receivedSignature) {
console.log('Le message est authentique !');
// Traiter les données de l'utilisateur
console.log('Données utilisateur :', userData);
} else {
console.error('La vérification de la signature du message a échoué !');
}
});
Remarques Importantes :
- Remplacez
VOTRE_SECRET_PARTAGE_SECURISEpar une clé secrète partagée générée de manière sécurisée. - La clé secrète partagée doit être la même dans l'iframe et dans la page parente.
- Cet exemple utilise HMAC-SHA256 pour l'authentification des messages.
Conclusion
L'API postMessage est un outil puissant pour permettre la communication cross-origine dans les applications web. Cependant, il est crucial de comprendre les risques de sécurité potentiels et de mettre en œuvre des modèles de sécurité appropriés pour atténuer ces risques. En suivant les modèles de sécurité et les meilleures pratiques décrits dans ce guide, vous pouvez utiliser postMessage en toute sécurité pour construire des applications web robustes et sécurisées.
N'oubliez pas de toujours donner la priorité à la sécurité et de vous tenir au courant des dernières meilleures pratiques en matière de développement web. Examinez régulièrement votre code et vos configurations de sécurité pour vous assurer que vos applications sont protégées contre les vulnérabilités potentielles.